package fr.bludwarf.skywarrior.global;

import java.io.File;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Vector;

import org.apache.commons.io.FilenameUtils;

import fr.bludwarf.skywarrior.conditions.Condition;
import fr.bludwarf.skywarrior.conditions.DansLePasse;
import fr.bludwarf.skywarrior.conditions.QqnPeutConstruire;
import fr.bludwarf.skywarrior.evenements.CommencerConstruction;
import fr.bludwarf.skywarrior.evenements.Evenement;
import fr.bludwarf.skywarrior.exceptions.IleInconnueException;
import fr.bludwarf.skywarrior.exceptions.JoueurInconnuException;
import fr.bludwarf.skywarrior.ile.EtatIle;
import fr.bludwarf.skywarrior.ile.Ile;
import fr.bludwarf.skywarrior.joueur.EtatJoueur;
import fr.bludwarf.skywarrior.joueur.Joueur;
import fr.bludwarf.skywarrior.joueur.Ressources;
import fr.bludwarf.skywarrior.joueur.Unite;
import fr.bludwarf.skywarrior.xml.PartieXML;

/**
 * Simulation d'une partie
 * @since 15 nov. 2011
 */
public class Partie {
	
//	private static final String EVENT_STRING = "%s (Δt = %12s) : %s";
	private static final String EVENT_STRING = "%s : %s";
	private static final SimpleDateFormat SDF_DEBUT_PARTIE = new SimpleDateFormat("dd/MM/yyyy HH:mm:SS");
	// FIXME : bizarre d'avoir à la fois une liste d'états d'un monde et un monde
	Vector<EtatMonde> _etats = new Vector<EtatMonde>();
	
	/** Numéro de la partie sur le serveur Skywar */
	final int _id;
	
	/** Début de la partie */
	final Calendar _debut;
	private Monde _monde;
	
	public Partie(final int id, final Calendar debut) {
		_id = id;
		_debut = debut;
	}
	
	public Partie(int id, Temps temps)
	{
		this(id, temps.getDebut());
	}

	public EtatMonde avancer() throws Exception
	{
		return avancer(new QqnPeutConstruire());
	}
	
	public EtatMonde avancer(final Condition condition) throws Exception
	{
		// Etat final
		EtatMonde etatFinal = getEtat();
		
		Evenement prochainEvenement = null;
		EtatMonde etatDernierEventAffiche = etatFinal;

		// S'il y a un évènement avant la fin de l'avance, on lance l'évènement
		while (condition.estVraie(getEtat()))
		{
			
			// Etat sur lequel executer l'évènement
			final EtatMonde etat = new EtatMonde(getEtat());
			
			prochainEvenement = etat.prochainEvenement();
			final double tempsProchainEvent = prochainEvenement.tempsRestant();
			
			// On avance d'un certain temps
			etat.avancer(tempsProchainEvent);
			etat.setEvenementDeclencheur(prochainEvenement);
			
			// On exécute l'évènement sur l'état
			prochainEvenement.execute(etat);
			
			// Affichage des évènements visibles
			if (prochainEvenement.estVisible())
			{
//				final Temps fin = new Temps(getTemps(), tempsProchainEvent);
				final Temps fin = etat.getTemps();
				final double deltaT = etatDernierEventAffiche.tempsEcouleDepuis(etat);
				
				PrintStream out = System.out;
				if (prochainEvenement instanceof CommencerConstruction && deltaT > 0)
				{
					out = System.err;
				}
				
				out.println(String.format(EVENT_STRING,
					fin.toCompleteString(),
//					Temps.toString(deltaT),
					prochainEvenement)
				);
//				out.println(String.format(EVENT_STRING,
//					fin.toCompleteString(),
//					Temps.toString(deltaT),
//					"Etat avant = " + etat)
//				);
				System.out.println(etat);
				etatDernierEventAffiche = etat;
			}
			
			// On sauvegarde le nouvel état
			_etats.add(etat);
			etatFinal = etat;
		}
		
		final Temps temps = etatFinal.getTemps();
		System.out.println(temps.toCompleteString() + " : FIN SIMULATION");
		System.out.println("Temps écoulé : " + Temps.toString(temps.tempsEcouleDepuis(_debut)));
		
		return etatFinal;
	}

	/**
	 * Lance une simulation sur une partie déjà démarrée
	 * @param actuel temps où l'on a relancé la partie
	 * @throws Exception 
	 * @since 10 nov. 2011
	 */
	public void lancer(final Calendar actuel) throws Exception
	{
		// Etat intial
		final Temps debut = new Temps(_debut);
		System.out.println(debut.toCompleteString() + " : DEBUT PARTIE");
		
		// état actuel
//		final Temps tempsActuel = new Temps(_debut, Temps.tempsEcoule(debut._debut, actuel));
		final Temps tempsActuel = new Temps(actuel, 0);
		System.out.println(tempsActuel.toCompleteString() + " : DEBUT SIMULATION");
		
		avancer();
	}
	
//	/**
//	 * Lance une simulation maintenant sur une partie déjà démarrée
//	 * @param actuel
//	 * @since 10 nov. 2011
//	 */
//	public void lancer()
//	{
//		lancer(Calendar.getInstance());
//	}
	
	/**
	 * Lance une simulationdepuis le début de la partie
	 * @param actuel
	 * @throws Exception 
	 * @since 10 nov. 2011
	 */
	public void lancer() throws Exception
	{
		lancer(_debut);
	}
	
	
	public EtatMonde getEtatInitial()
	{
		return _etats.get(0);
	}
	
	public EtatMonde getEtat()
	{
		return _etats.lastElement();
	}
	
	public EtatMonde getEtat(Calendar date) throws Exception
	{
		// Etat juste avant la date
		EtatMonde etatFloor = null;
		
		for (final EtatMonde etat : _etats)
		{
			if (etatFloor != null && etat.getTemps().getDateReelle().compareTo(date) > 0)
			{
				break;
			}
			etatFloor = etat;
		}
		
		// On fait une projection de l'état (car il n'y a aucun évènement entre deux états)
		final double temps = Temps.tempsEcoule(etatFloor.getTemps().getDateReelle(), date);
		final EtatMonde etat = new EtatMonde(etatFloor);
		etat.avancer(temps);
		
		return etat;
	}
	
	public EtatMonde getEtatActuel() throws Exception
	{
		avancer(new DansLePasse());
		return getEtatFinal();
	}
	
	public EtatMonde getEtatFinal() throws Exception
	{
		return _etats.lastElement();
	}
	
	public Temps getTemps()
	{
		return getEtat().getTemps();
	}
	
	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();

		sb.append("====================== PARTIE commencée le ").append(getEtatInitial().getTemps().formatDateReelle())
				.append(" ======================");
		
		for (final EtatMonde monde : _etats)
		{
			sb.append("\n\n").append(monde);
		}

		return sb.toString();
	}

	public void ajouterJoueur(String nomJoueur, String nomIle) throws JoueurInconnuException
	{
		getMonde().ajouterJoueur(nomJoueur, nomIle);
	}
	
	public Monde getMonde()
	{
		if (_monde == null)
		{
			_monde = new Monde(getId());
		}
		return _monde;
	}

	/**
	 * Ajoute une construction dans la liste des construction d'une ile
	 * @param unite unité à construire
	 * @param nomIle nom de l'ile sur laquelle on souhaite construire l'unité
	 * @throws IleInconnueException
	 * @throws TimeFactorInconnu si le TimeFactor est inconnu pour le type demandé
	 * @since 4 nov. 2011
	 */
	public void construire(Unite unite, String nomIle) throws IleInconnueException, TimeFactorInconnu {
		final Ile ile = getMonde().getIle(nomIle);
		construire(unite, ile);
	}

	/**
	 * @param unite
	 * @param ile
	 * @throws TimeFactorInconnu
	 */
	protected void construire(Unite unite, final Ile ile)
			throws TimeFactorInconnu
	{
		final EtatIle etatIle = getEtat()
				.getEtatJoueur(ile.getJoueur())
				.getEtatIle(ile);
		etatIle.construire(unite);
	}

	public void construire(Unite unite) throws TimeFactorInconnu
	{
		construire(unite, getMonde().getSeuleIle());
	}

	public void construire(Unite ... unites) throws TimeFactorInconnu
	{
		for (final Unite unite : unites)
		{
			construire(unite, getMonde().getSeuleIle());
		}
	}

	/**
	 * Ajoute n construction(s) dans la liste des construction d'une ile
	 * @param n nombre d'unité(s) à construire
	 * @param unite unité à construire
	 * @param nomIle nom de l'ile sur laquelle on souhaite construire l'unité
	 * @throws IleInconnueException 
	 * @throws TimeFactorInconnu si le TimeFactor est inconnu pour le type demandé
	 * @since 4 nov. 2011
	 */
	public void construire(final int n, Unite unite, String nomIle) throws IleInconnueException, TimeFactorInconnu {
		for (int i = 0; i < n; ++i) {
			construire(unite, nomIle);
		}
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Partie)
		{
			return ((Partie) obj)._id == this._id;
		}
		else if (obj instanceof Number)
		{
			return ((Number) obj).equals(this._id);
		}
		else
		{
			return false;
		}
	}

	/**
	 * @param date sous forme jj/mm/aaaa hh:mm:ss
	 * @return
	 * @throws Exception 
	 * @since 17 nov. 2011
	 */
	public EtatMonde getEtat(String dateStr) throws Exception {
		final Calendar cal = Calendar.getInstance();
		final Date date = SDF_DEBUT_PARTIE.parse(dateStr);
		cal.setTime(date);
		return getEtat(cal);
	}

	// TODO : stocker la StackTrace pour savoir d'où vient l'ordre qui a fait planter la simu
	// ou alors indiquer quel # de construction à planter
	public void construire(int nb, Unite unite) throws TimeFactorInconnu
	{
		for (int i = 0; i < nb; ++i)
		{
			construire(unite);
		}
	}

	public int getId()
	{
		return _id;
	}

	public void add(EtatMonde etatMonde)
	{
		_etats.add(etatMonde);
	}

	public Calendar getDebut()
	{
		return _debut;
	}
	
	public void save(final File file) throws Exception
	{		
		// save
		final PartieXML xml = new PartieXML(this);
		xml.save(file);
	}
	
	public static Partie load(File file) throws Exception
	{		
		// save
		final PartieXML xml = new PartieXML();
		return xml.load(file);
	}

	public EtatIle getEtatIle(Ile ile) throws IleInconnueException
	{
		return getEtat().getEtatIle(ile);
	}

	public Joueur getJoueurPrincipal() throws JoueurInconnuException
	{
		return getEtat().getJoueurPrincipal();
	}

	public Ressources getProduction() throws JoueurInconnuException
	{
		return getEtatJoueur(getJoueurPrincipal()).getProduction();
	}

	public EtatJoueur getEtatJoueur(Joueur joueur)
	{
		return getEtat().getEtatJoueur(joueur);
	}
}
